/*
 * Copyright 2002-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baidu.bjf.remoting.protobuf;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.baidu.bjf.remoting.protobuf.annotation.Protobuf;
import com.baidu.bjf.remoting.protobuf.utils.FieldInfo;
import com.baidu.bjf.remoting.protobuf.utils.FieldUtils;
import com.baidu.bjf.remoting.protobuf.utils.ProtobufProxyUtils;
import com.google.protobuf.ByteString;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.UninitializedMessageException;
import com.google.protobuf.WireFormat;

/**
 * A reflective usage by java reflect utility tools
 * 
 * @author xiemalin
 * @since 1.1.0
 */
public class ReflectiveCodec<T> implements Codec<T> {

	private Class<T> cls;

	private Map<Integer, FieldInfo> orderFieldsMapping;
	private List<FieldInfo> fieldInfos;

	public ReflectiveCodec(Class<T> cls) {
		this.cls = cls;

		List<Field> fields = FieldUtils.findMatchedFields(cls, Protobuf.class);
		if (fields.isEmpty()) {
			throw new IllegalArgumentException("Invalid class [" + cls.getName() + "] no field use annotation @"
					+ Protobuf.class.getName() + " at class " + cls.getName());
		}

		fieldInfos = ProtobufProxyUtils.processDefaultValue(fields);

		orderFieldsMapping = new HashMap<Integer, FieldInfo>();
		for (FieldInfo fieldInfo : fieldInfos) {
			int tag = CodedConstant.makeTag(fieldInfo.getOrder(),
					fieldInfo.getFieldType().getInternalFieldType().getWireType());
			orderFieldsMapping.put(tag, fieldInfo);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.baidu.bjf.remoting.protobuf.Codec#encode(java.lang.Object)
	 */
	@Override
	public byte[] encode(T t) throws IOException {
		if (t == null) {
			throw new RuntimeException("target object to encode is null.");
		}

		int size = size(t);
		byte[] bytes = new byte[size];
		CodedOutputStream out = CodedOutputStream.newInstance(bytes);
		writeTo(t, out);

		return bytes;
	}

	private int computeSize(FieldInfo fieldInfo, Object value) throws IOException {
		FieldType fieldType = fieldInfo.getFieldType();

		int size = 0;
		if (value instanceof List) {
			// if list
			size = CodedConstant.computeListSize(fieldInfo.getOrder(), (List) value, fieldInfo.getFieldType(), true, null);
			return size;
		}

		int order = fieldInfo.getOrder();
		switch (fieldType) {
		case DOUBLE:
			size = CodedOutputStream.computeDoubleSize(order, (Double) value);
			break;
		case BYTES:
			ByteString bytes = ByteString.copyFrom((byte[]) value);
			size = CodedOutputStream.computeBytesSize(order, bytes);
			break;
		case STRING:
			ByteString string = ByteString.copyFromUtf8(value.toString());
			size = CodedOutputStream.computeBytesSize(order, string);
			break;
		case BOOL:
			size = CodedOutputStream.computeBoolSize(order, (Boolean) value);
			break;
		case FIXED32:
			size = CodedOutputStream.computeFixed32Size(order, (Integer) value);
			break;
		case SFIXED32:
			size = CodedOutputStream.computeSFixed32Size(order, (Integer) value);
			break;
		case SINT32:
			size = CodedOutputStream.computeSInt32Size(order, (Integer) value);
			break;
		case INT32:
			size = CodedOutputStream.computeInt32Size(order, (Integer) value);
			break;
		case UINT32:
			size = CodedOutputStream.computeUInt32Size(order, (Integer) value);
			break;
		case FIXED64:
			size = CodedOutputStream.computeFixed64Size(order, (Long) value);
			break;
		case SFIXED64:
			size = CodedOutputStream.computeSFixed64Size(order, (Long) value);
			break;
		case SINT64:
			size = CodedOutputStream.computeSInt64Size(order, (Long) value);
			break;
		case INT64:
			size = CodedOutputStream.computeInt64Size(order, (Long) value);
			break;
		case UINT64:
			size = CodedOutputStream.computeUInt64Size(order, (Long) value);
			break;
		case ENUM:
			int i;
			i = getEnumValue(value);
			size = CodedOutputStream.computeEnumSize(order, i);
			break;
		case FLOAT:
			size = CodedOutputStream.computeFloatSize(order, (Float) value);
			break;
		case OBJECT:
			Class c = value.getClass();
			ReflectiveCodec codec = new ReflectiveCodec(c);
			
			int objectSize = codec.size(value);
			
			size = size + CodedOutputStream.computeRawVarint32Size(objectSize);
			size = size + CodedOutputStream.computeTagSize(order);
			
			size += objectSize;
			break;
		default:
			throw new IOException("Unknown field type on field '" + fieldInfo.getField().getName() + "'");
		}

		return size;
	}

	private int getEnumValue(Object value) {
		int i;
		if (value instanceof EnumReadable) {
			i = ((EnumReadable) value).value();
		} else {
			Enum e = (Enum) value;
			i = e.ordinal();
		}
		return i;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.baidu.bjf.remoting.protobuf.Codec#decode(byte[])
	 */
	@Override
	public T decode(byte[] bytes) throws IOException {
		if (bytes == null) {
			throw new IOException("byte array is null.");
		}
		CodedInputStream input = CodedInputStream.newInstance(bytes, 0, bytes.length);

		return readFrom(input);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.baidu.bjf.remoting.protobuf.Codec#size(java.lang.Object)
	 */
	@Override
	public int size(T t) throws IOException {
		int size = 0;
		for (FieldInfo fieldInfo : fieldInfos) {
			Object value = FieldUtils.getField(t, fieldInfo.getField());
			// to check required
			if (value == null) {
				if (fieldInfo.isRequired()) {
					throw new UninitializedMessageException(CodedConstant.asList(fieldInfo.getField().getName()));
				}
			} else {
				size += computeSize(fieldInfo, value);
			}

		}
		return size;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.baidu.bjf.remoting.protobuf.Codec#writeTo(java.lang.Object,
	 * com.google.protobuf.CodedOutputStream)
	 */
	@Override
	public void writeTo(T t, CodedOutputStream out) throws IOException {
		for (FieldInfo fieldInfo : fieldInfos) {
			Object value = FieldUtils.getField(t, fieldInfo.getField());
			if (value != null) {
				writeTo(fieldInfo, value, out);
			}
		}

	}

	private void writeTo(FieldInfo fieldInfo, Object value, CodedOutputStream out) throws IOException {
		FieldType fieldType = fieldInfo.getFieldType();
		int order = fieldInfo.getOrder();

		if (value instanceof List) {
			// if check list
			CodedConstant.writeToList(out, order, fieldType, (List) value);
			return;
		}

		switch (fieldType) {
		case DOUBLE:
			out.writeDouble(order, (Double) value);
			break;
		case BYTES:
			ByteString bytes = ByteString.copyFrom((byte[]) value);
			out.writeBytes(order, bytes);
			break;
		case STRING:
			ByteString string = ByteString.copyFromUtf8(value.toString());
			out.writeBytes(order, string);
			break;
		case BOOL:
			out.writeBool(order, (Boolean) value);
			break;
		case FIXED32:
			out.writeFixed32(order, (Integer) value);
			break;
		case SFIXED32:
			out.writeSFixed32(order, (Integer) value);
			break;
		case SINT32:
			out.writeSInt32(order, (Integer) value);
			break;
		case INT32:
			out.writeInt32(order, (Integer) value);
			break;
		case UINT32:
			out.writeUInt32(order, (Integer) value);
			break;
		case FIXED64:
			out.writeFixed64(order, (Long) value);
			break;
		case SFIXED64:
			out.writeSFixed64(order, (Long) value);
			break;
		case SINT64:
			out.writeSInt64(order, (Long) value);
			break;
		case INT64:
			out.writeInt64(order, (Long) value);
			break;
		case UINT64:
			out.writeUInt64(order, (Long) value);
			break;
		case ENUM:
			int i;
			i = getEnumValue(value);
			out.writeEnum(order, i);
			break;
		case FLOAT:
			out.writeFloat(order, (Float) value);
			break;
		case OBJECT:
			Class c = value.getClass();
			ReflectiveCodec codec = new ReflectiveCodec(c);
			out.writeRawVarint32(CodedConstant.makeTag(order, WireFormat.WIRETYPE_LENGTH_DELIMITED));
			out.writeRawVarint32(codec.size(value));
			codec.writeTo(value, out);
			break;
		default:
			throw new IOException("Unknown field type on field '" + fieldInfo.getField().getName() + "'");
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.baidu.bjf.remoting.protobuf.Codec#readFrom(com.google.protobuf.
	 * CodedInputStream)
	 */
	@Override
	public T readFrom(CodedInputStream input) throws IOException {
		T t;
		try {
			t = cls.newInstance();
		} catch (InstantiationException e1) {
			throw new IOException(e1.getMessage(), e1);
		} catch (IllegalAccessException e1) {
			throw new IOException(e1.getMessage(), e1);
		}
		try {
			boolean done = false;
			while (!done) {
				int tag = input.readTag();
				if (tag == 0) {
					break;
				}

				FieldInfo fieldInfo = orderFieldsMapping.get(tag);
				if (fieldInfo == null) {
					input.skipField(tag);
					// maybe new field added should be ignore
					continue;
				}
				
				// check list type
				Field field = fieldInfo.getField();
				Class<?> c = fieldInfo.getField().getType();
				if (List.class.isAssignableFrom(c)) {
					List list = (List) FieldUtils.getField(t, field);
					if (list == null) {
						list = new ArrayList();
						FieldUtils.setField(t, field, list);
					}
					list.add(readValue(input, fieldInfo));
					continue;
				}

				FieldType fieldType = fieldInfo.getFieldType();
				switch (fieldType) {
				case DOUBLE:
					FieldUtils.setField(t, field, input.readDouble());
					break;
				case BYTES:
					FieldUtils.setField(t, field, input.readBytes().toByteArray());
					break;
				case STRING:
					FieldUtils.setField(t, field, input.readString());
					break;
				case BOOL:
					FieldUtils.setField(t, field, input.readBool());
					break;
				case FIXED32:
					FieldUtils.setField(t, field, input.readFixed32());
					break;
				case SFIXED32:
					FieldUtils.setField(t, field, input.readSFixed32());
					break;
				case SINT32:
					FieldUtils.setField(t, field, input.readSInt32());
					break;
				case INT32:
					FieldUtils.setField(t, field, input.readInt32());
					break;
				case UINT32:
					FieldUtils.setField(t, field, input.readUInt32());
					break;
				case FIXED64:
					FieldUtils.setField(t, field, input.readFixed64());
					break;
				case SFIXED64:
					FieldUtils.setField(t, field, input.readSFixed64());
					break;
				case SINT64:
					FieldUtils.setField(t, field, input.readSInt64());
					break;
				case INT64:
					FieldUtils.setField(t, field, input.readInt64());
					break;
				case UINT64:
					FieldUtils.setField(t, field, input.readUInt64());
					break;
				case ENUM:
					Class<?> type = field.getType();
					try {
						Method method = type.getMethod("values");
						Enum[] inter = (Enum[]) method.invoke(null, null);

						if (Enum.class.isAssignableFrom(type)) {
							Enum value = CodedConstant.getEnum(inter, input.readEnum());
							FieldUtils.setField(t, fieldInfo.getField(), value);
						}
					} catch (Exception e) {
						throw new IOException(e.getMessage(), e);
					}
					break;
				case FLOAT:
					FieldUtils.setField(t, field, input.readFloat());
					break;
				case OBJECT:
					ReflectiveCodec codec = new ReflectiveCodec(c);

					int length = input.readRawVarint32();
					final int oldLimit = input.pushLimit(length);
					Object o = codec.readFrom(input);
					FieldUtils.setField(t, fieldInfo.getField(), o);
					break;
				default:
					throw new IOException("Unknown field type on field '" + fieldInfo.getField().getName() + "'");
				}

			}

		} catch (com.google.protobuf.InvalidProtocolBufferException e) {
			throw e;
		} catch (java.io.IOException e) {
			throw e;
		}

		return t;
	}
	
	private Object readValue(CodedInputStream input, FieldInfo fieldInfo) throws IOException {
		FieldType fieldType = fieldInfo.getFieldType();
		switch (fieldType) {
		case DOUBLE:
			return input.readDouble();
		case BYTES:
			return input.readBytes().toByteArray();
		case STRING:
			return input.readString();
		case BOOL:
			return input.readBool();
		case FIXED32:
			return input.readFixed32();
		case SFIXED32:
			return input.readSFixed32();
		case SINT32:
			return input.readSInt32();
		case INT32:
			return input.readInt32();
		case UINT32:
			return input.readUInt32();
		case FIXED64:
			return input.readFixed64();
		case SFIXED64:
			return input.readSFixed64();
		case SINT64:
			return input.readSInt64();
		case INT64:
			return input.readInt64();
		case UINT64:
			return input.readUInt64();
		case ENUM:
                        Class<?> type = null;
                        if(fieldInfo.isList()) {
                           type = fieldInfo.getGenericKeyType();
                        } else{
                            type = fieldInfo.getField().getType();
                        }
			try {
				Method method = type.getMethod("values");
				Enum[] inter = (Enum[]) method.invoke(null, null);

				if (Enum.class.isAssignableFrom(type)) {
					Enum value = CodedConstant.getEnum(inter, input.readEnum());
					return value;
				}
			} catch (Exception e) {
				throw new IOException(e.getMessage(), e);
			}
			break;
		case FLOAT:
			return input.readFloat();
		case OBJECT:
                        if(fieldInfo.isList()) {
                            Class<?> c = fieldInfo.getGenericKeyType();
                            ReflectiveCodec codec = new ReflectiveCodec(c);

                            int length = input.readRawVarint32();
                            final int oldLimit = input.pushLimit(length);
                            Object o = codec.readFrom(input);
                            input.checkLastTagWas(0);
                            input.popLimit(oldLimit);
                            return o;
                        }
			Class<?> c = fieldInfo.getField().getType();
			ReflectiveCodec codec = new ReflectiveCodec(c);

			int length = input.readRawVarint32();
			final int oldLimit = input.pushLimit(length);
			Object o = codec.readFrom(input);
			return o;
		default:
			throw new IOException("Unknown field type on field '" + fieldInfo.getField().getName() + "'");
		}
		
		return null;
	}

}
